<?php
// Copyright 2019 Hackware SpA <human@hackware.cl>
// "Hackware Web Services Core" is released under the MIT License terms.

namespace Hawese\Core;

use Hawese\Core\Exceptions\WrongCredentialsException;
use Illuminate\Cache\RateLimiter;
use RuntimeException;

class User extends TableModel
{
    public static $table = 'users';
    public static $attributes = [
        'uid' => ['required', 'string', 'min:3', 'max:100'],
        'email' => ['nullable', 'email', 'min:3', 'max:100'],
        'password' => ['nullable', 'string', 'min:6'],
        'display_name' => ['nullable', 'string', 'min:3', 'max:100'],
        'info' => ['nullable', 'json', 'max:65535'],
        'created_at' => ['nullable', 'date'],
        'updated_at' => ['nullable', 'date'],
        'deleted_at' => ['nullable', 'date'],
    ];
    protected $hidden = ['password', 'deleted_at'];

    public static $primary_key = 'uid';
    protected static $incrementing = false;

    public function changePassword(string $password): self
    {
        $this->password = password_hash($password, PASSWORD_DEFAULT);
        return $this;
    }

    /**
     * @param string $username can be the uid or email
     * @param string $password plain text password
     * @param bool $remember for automatic login ("remember me")
     */
    public static function loginByPassword(
        string $username,
        string $password,
        bool $remember = false
    ): self {
        self::abortIfTooManyFailedLogins($username);

        $user = self::find($username, ['uid', 'email']);
        if (!password_verify($password, $user->password)) {
            self::increaseFailedLogins($username);
            throw new WrongCredentialsException('user', $username);
        }

        self::clearFailedLogins($username);

        app('session')->set('user_uid', $user->uid);
        app('session')->migrate();

        if ($remember) {
            $user->generateHumanToken(true);
        }

        return $user;
    }

    /**
     * Will login based on a token key and secret.
     *
     * If Token::type is HUMAN, it will be discarded and a new one
     * generated on the fly as are single use tokens (i.e. "remember me"
     * generated tokens), unless $remember is set to false (i.e.
     * email tokens) check AuthServiceProvider for better understanding.
     */
    public static function loginByToken(
        string $key,
        string $secret,
        bool $remember = true // Only useful for HUMAN tokens
    ): self {
        self::abortIfTooManyFailedLogins($key);

        $token = Token::find($key);
        if (!password_verify($secret, $token->secret)) {
            self::increaseFailedLogins($user);
            // TODO: Notify user
            throw new WrongCredentialsException('token', $key);
        }

        self::clearFailedLogins($key);
        $user = self::find($token->user_uid);

        if ($token->type == Token::HUMAN) {
            app('session')->set('user_uid', $user->uid);
            $token->delete();
            setcookie('auth_token', '', time() - 3600, '/', null);
            if ($remember) {
                $user->generateHumanToken(true);
            }
        }

        return $user;
    }

    public function generateHumanToken($set_cookie = false): Token
    {
        $token = Token::generate(Token::HUMAN, $this->uid);

        if ($set_cookie) {
            setcookie(
                'auth_token', // name
                "$token->key:$token->secret", // value
                time() + 30 * 24 * 60 * 60, // expires on 1 month
                '/', // path
                null, // domain origin. if set includes subdomains
                app()->environment() == 'production' ? true : false, // secure
                true, // httponly, don't allow reading through javascript
            );
        }

        return $token;
    }
    
    public function generateSystemToken(): Token
    {
        return Token::generate(Token::SYSTEM, $this->uid);
    }

    /**
     * @return int current hits for this $loginKey
     */
    private static function increaseFailedLogins(string $loginKey): int
    {
        // Failed logins in 1 minute
        return self::limiter()->hit(self::remoteId($loginKey), 60);
    }

    private static function clearFailedLogins(string $loginKey): void
    {
        self::limiter()->clear(self::remoteId($loginKey));
    }

    private static function abortIfTooManyFailedLogins(string $loginKey): void
    {
        // More than 4 attempts is too much
        if (self::limiter()->tooManyAttempts(self::remoteId($loginKey), 4)) {
            throw new RuntimeException(
                'Too many failed requests. Vamo a calmarno.',
                5
            );
        };
    }

    /**
     * Identifies a remote user
     * @param string $loginKey a login key value, such as `uid` or `secret`
     */
    private static function remoteId(string $loginKey): string
    {
        return strtolower($loginKey) . '|' . $_SERVER['REMOTE_ADDR'];
    }

    private static function limiter(): RateLimiter
    {
        return app(RateLimiter::class);
    }

    public function logout(): bool
    {
        if (array_key_exists('auth_token', $_COOKIE)) {
            list($key, $secret) = explode(':', $_COOKIE['auth_token']);
            Token::find($key)->delete();
            setcookie('auth_token', '', time() - 3600, '/', null);
        }

        return app('session')->invalidate();
    }
    
    /**
     * @param string $username can be the uid or email
     * @param string $origin_url $_SERVER['HTTP_REFERER'] | $_GET['origin_url']
     */
    public static function emailToken(
        string $username,
        string $origin_url,
        string $redirect_to = null
    ): self {
        self::validateOrigin($origin_url);

        $user = User::find($username, ['uid', 'email']);
        $token = $user->generateHumanToken();
        $email_token_link = sprintf(
            "%s/?auth_token=%s:%s",
            $origin_url,
            $token->key,
            $token->secret
        );

        if ($redirect_to) {
            $email_token_link .= '&redirect_to=' . rawurlencode($redirect_to);
        }

        self::sendEmailTokenEmail($user, $email_token_link);

        return $user;
    }

    private static function validateOrigin(string &$origin_url): void
    {
        $origins = config('cors.default_profile.allow_origins');
        if (in_array($origin_url, $origins) === false) {
            throw new RuntimeException('Unacceptable origin', 6);
        }
    }

    private static function sendEmailTokenEmail(
        self &$user,
        string &$email_token_link
    ): void {
        $mail = app(Mailer::class);

        $mail->addAddress($user->email, $user->display_name);
        $mail->Subject = __(
            'Tu enlace de inicio de sesión en :app_name',
            ['app_name' => config('app.name')]
        );
        $mail->Body = view(
            'core::emails/email_token_html',
            ['email_token_link' => $email_token_link]
        )->render();
        $mail->AltBody = view(
            'core::emails/email_token_txt',
            ['email_token_link' => $email_token_link]
        )->render();

        $mail->send();
    }

    // Users own themselves
    public function isOwner(self $user): bool
    {
        return $this->id === $user->id;
    }

    public function isSuperUser(): bool
    {
        return $this->uid === 'hawese';
    }
}
